/*
 * ImportedFurnitureWizardStepsPanel.java 4 juil. 07
 *
 * Sweet Home 3D, Copyright (c) 2007 Emmanuel PUYBARET / eTeks <info@eteks.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package com.eteks.sweethome3d.swing;

import java.awt.CardLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.ComponentOrientation;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicReference;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import javax.media.j3d.BranchGroup;
import javax.media.j3d.Transform3D;
import javax.swing.Action;
import javax.swing.ComboBoxEditor;
import javax.swing.DefaultListCellRenderer;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.JToolTip;
import javax.swing.JWindow;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.TransferHandler;
import javax.swing.UIManager;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.vecmath.Matrix3f;
import javax.vecmath.Vector3d;
import javax.vecmath.Vector3f;

import com.eteks.sweethome3d.j3d.ModelManager;
import com.eteks.sweethome3d.j3d.OBJWriter;
import com.eteks.sweethome3d.model.CatalogPieceOfFurniture;
import com.eteks.sweethome3d.model.Content;
import com.eteks.sweethome3d.model.FurnitureCategory;
import com.eteks.sweethome3d.model.RecorderException;
import com.eteks.sweethome3d.model.UserPreferences;
import com.eteks.sweethome3d.tools.OperatingSystem;
import com.eteks.sweethome3d.tools.TemporaryURLContent;
import com.eteks.sweethome3d.tools.URLContent;
import com.eteks.sweethome3d.viewcontroller.ContentManager;
import com.eteks.sweethome3d.viewcontroller.ImportedFurnitureWizardController;
import com.eteks.sweethome3d.viewcontroller.ImportedFurnitureWizardStepsView;

/**
 * Wizard panel for furniture import. 
 * @author Emmanuel Puybaret
 */
public class ImportedFurnitureWizardStepsPanel extends JPanel implements ImportedFurnitureWizardStepsView
{
	private final ImportedFurnitureWizardController controller;
	private CardLayout cardLayout;
	private JLabel modelChoiceOrChangeLabel;
	private JButton modelChoiceOrChangeButton;
	private JButton findModelsButton;
	private JLabel modelChoiceErrorLabel;
	private ModelPreviewComponent modelPreviewComponent;
	private JLabel orientationLabel;
	private JButton defaultOrientationButton;
	private JButton turnLeftButton;
	private JButton turnRightButton;
	private JButton turnUpButton;
	private JButton turnDownButton;
	private int horizontalAngle;
	private int verticalAngle;
	private JToolTip orientationToolTip;
	private JWindow orientationToolTipWindow;
	private RotationPreviewComponent rotationPreviewComponent;
	private JLabel backFaceShownLabel;
	private JCheckBox backFaceShownCheckBox;
	private JLabel attributesLabel;
	private JLabel nameLabel;
	private JTextField nameTextField;
	private JCheckBox addToCatalogCheckBox;
	private JLabel categoryLabel;
	private JComboBox categoryComboBox;
	private JLabel widthLabel;
	private JSpinner widthSpinner;
	private JLabel depthLabel;
	private JSpinner depthSpinner;
	private JLabel heightLabel;
	private JSpinner heightSpinner;
	private JCheckBox keepProportionsCheckBox;
	private JLabel elevationLabel;
	private JSpinner elevationSpinner;
	private AttributesPreviewComponent attributesPreviewComponent;
	private JCheckBox movableCheckBox;
	private JCheckBox doorOrWindowCheckBox;
	private JCheckBox staircaseCheckBox;
	private JLabel colorLabel;
	private ColorButton colorButton;
	private JButton clearColorButton;
	private JLabel iconLabel;
	private IconPreviewComponent iconPreviewComponent;
	private Cursor defaultCursor;
	private Executor modelLoader;
	
	/**
	 * Creates a view for furniture import. 
	 */
	public ImportedFurnitureWizardStepsPanel(CatalogPieceOfFurniture piece, String modelName, boolean importHomePiece,
			UserPreferences preferences, final ImportedFurnitureWizardController controller)
	{
		this.controller = controller;
		// Create a model loader for each wizard in case model loading hangs
		this.modelLoader = Executors.newSingleThreadExecutor();
		createComponents(importHomePiece, preferences, controller);
		setMnemonics(preferences);
		layoutComponents();
		updateController(piece, preferences);
		if (modelName != null)
		{
			updateController(modelName, preferences, controller.getContentManager(),
					importHomePiece ? null : preferences.getFurnitureCatalog().getCategories().get(0), true);
		}
		
		controller.addPropertyChangeListener(ImportedFurnitureWizardController.Property.STEP,
				new PropertyChangeListener()
				{
					public void propertyChange(PropertyChangeEvent evt)
					{
						updateStep(controller);
					}
				});
	}
	
	/**
	 * Creates components displayed by this panel.
	 */
	private void createComponents(final boolean importHomePiece, final UserPreferences preferences,
			final ImportedFurnitureWizardController controller)
	{
		// Get unit name matching current unit 
		String unitName = preferences.getLengthUnit().getName();
		
		// Model panel components
		this.modelChoiceOrChangeLabel = new JLabel();
		this.modelChoiceOrChangeButton = new JButton();
		final FurnitureCategory defaultModelCategory = (importHomePiece
				|| preferences.getFurnitureCatalog().getCategories().size() == 0) ? null
						: preferences.getFurnitureCatalog().getCategories().get(0);
		this.modelChoiceOrChangeButton.addActionListener(new ActionListener()
		{
			public void actionPerformed(ActionEvent ev)
			{
				String modelName = showModelChoiceDialog(preferences, controller.getContentManager());
				if (modelName != null)
				{
					updateController(modelName, preferences, controller.getContentManager(), defaultModelCategory,
							false);
				}
			}
		});
		try
		{
			this.findModelsButton = new JButton(SwingTools.getLocalizedLabelText(preferences,
					ImportedFurnitureWizardStepsPanel.class, "findModelsButton.text"));
			final String findModelsUrl = preferences.getLocalizedString(ImportedFurnitureWizardStepsPanel.class,
					"findModelsButton.url");
			this.findModelsButton.addActionListener(new ActionListener()
			{
				public void actionPerformed(ActionEvent ev)
				{
					boolean documentShown = false;
					try
					{
						// Display Find models page in browser
						documentShown = SwingTools.showDocumentInBrowser(new URL(findModelsUrl));
					}
					catch (MalformedURLException ex)
					{
						// Document isn't shown
					}
					if (!documentShown)
					{
						// If the document wasn't shown, display a message 
						// with a copiable URL in a message box 
						JTextArea findModelsMessageTextArea = new JTextArea(preferences
								.getLocalizedString(ImportedFurnitureWizardStepsPanel.class, "findModelsMessage.text"));
						String findModelsTitle = preferences.getLocalizedString(ImportedFurnitureWizardStepsPanel.class,
								"findModelsMessage.title");
						findModelsMessageTextArea.setEditable(false);
						findModelsMessageTextArea.setOpaque(false);
						JOptionPane.showMessageDialog(
								SwingUtilities.getRootPane(ImportedFurnitureWizardStepsPanel.this),
								findModelsMessageTextArea, findModelsTitle, JOptionPane.INFORMATION_MESSAGE);
					}
				}
			});
		}
		catch (IllegalArgumentException ex)
		{
			// Don't create findModelsButton if its text or url isn't defined
		}
		this.modelChoiceErrorLabel = new JLabel(
				preferences.getLocalizedString(ImportedFurnitureWizardStepsPanel.class, "modelChoiceErrorLabel.text"));
		// Make modelChoiceErrorLabel visible only if an error occurred during model content loading
		this.modelChoiceErrorLabel.setVisible(false);
		this.modelPreviewComponent = new ModelPreviewComponent(true);
		// Add a transfer handler to model preview component to let user drag and drop a file in component
		this.modelPreviewComponent.setTransferHandler(new TransferHandler()
		{
			@Override
			public boolean canImport(JComponent comp, DataFlavor[] flavors)
			{
				return Arrays.asList(flavors).contains(DataFlavor.javaFileListFlavor);
			}
			
			@Override
			public boolean importData(JComponent comp, Transferable transferedFiles)
			{
				boolean success = false;
				try
				{
					List<File> files = (List<File>) transferedFiles.getTransferData(DataFlavor.javaFileListFlavor);
					for (File file : files)
					{
						final String modelName = file.getAbsolutePath();
						// Try to import the first file that would be accepted by content manager
						if (controller.getContentManager().isAcceptable(modelName, ContentManager.ContentType.MODEL))
						{
							EventQueue.invokeLater(new Runnable()
							{
								public void run()
								{
									updateController(modelName, preferences, controller.getContentManager(),
											defaultModelCategory, false);
								}
							});
							success = true;
							break;
						}
					}
				}
				catch (UnsupportedFlavorException ex)
				{
					// No success
				}
				catch (IOException ex)
				{
					// No success
				}
				if (!success)
				{
					EventQueue.invokeLater(new Runnable()
					{
						public void run()
						{
							JOptionPane.showMessageDialog(
									SwingUtilities.getRootPane(ImportedFurnitureWizardStepsPanel.this),
									preferences.getLocalizedString(ImportedFurnitureWizardStepsPanel.class,
											"modelChoiceErrorLabel.text"));
						}
					});
				}
				return success;
			}
		});
		this.modelPreviewComponent.setBorder(SwingTools.getDropableComponentBorder());
		
		// Orientation panel components
		this.orientationLabel = new JLabel(
				preferences.getLocalizedString(ImportedFurnitureWizardStepsPanel.class, "orientationLabel.text"));
		this.defaultOrientationButton = new JButton(
				new ResourceAction(preferences, ImportedFurnitureWizardStepsPanel.class, "DEFAULT_ORIENTATION", true)
				{
					@Override
					public void actionPerformed(ActionEvent ev)
					{
						updateModelRotation(new Transform3D());
						horizontalAngle = 0;
						verticalAngle = 0;
					}
				});
		final String angleTooltipFormat = preferences.getLocalizedString(ImportedFurnitureWizardStepsPanel.class,
				"angleTooltipFeedback");
		this.orientationToolTip = new JToolTip();
		this.turnLeftButton = new AutoRepeatButton(
				new ResourceAction(preferences, ImportedFurnitureWizardStepsPanel.class, "TURN_LEFT", true)
				{
					@Override
					public void actionPerformed(ActionEvent ev)
					{
						Transform3D oldTransform = getModelRotationTransform();
						Transform3D leftRotation = new Transform3D();
						int deltaAngle = (ev.getModifiers() & ActionEvent.SHIFT_MASK) == 0 ? -90 : -1;
						leftRotation.rotY(Math.toRadians(deltaAngle));
						leftRotation.mul(oldTransform);
						updateModelRotation(leftRotation);
						horizontalAngle = (horizontalAngle + deltaAngle) % 360;
						orientationToolTip.setTipText(String.format(angleTooltipFormat, horizontalAngle));
						verticalAngle = 0;
					}
				});
		this.turnRightButton = new AutoRepeatButton(
				new ResourceAction(preferences, ImportedFurnitureWizardStepsPanel.class, "TURN_RIGHT", true)
				{
					@Override
					public void actionPerformed(ActionEvent ev)
					{
						Transform3D oldTransform = getModelRotationTransform();
						Transform3D rightRotation = new Transform3D();
						int deltaAngle = (ev.getModifiers() & ActionEvent.SHIFT_MASK) == 0 ? 90 : 1;
						rightRotation.rotY(Math.toRadians(deltaAngle));
						rightRotation.mul(oldTransform);
						updateModelRotation(rightRotation);
						horizontalAngle = (horizontalAngle + deltaAngle) % 360;
						orientationToolTip.setTipText(String.format(angleTooltipFormat, horizontalAngle));
						verticalAngle = 0;
					}
				});
		this.turnUpButton = new AutoRepeatButton(
				new ResourceAction(preferences, ImportedFurnitureWizardStepsPanel.class, "TURN_UP", true)
				{
					@Override
					public void actionPerformed(ActionEvent ev)
					{
						Transform3D oldTransform = getModelRotationTransform();
						Transform3D upRotation = new Transform3D();
						int deltaAngle = (ev.getModifiers() & ActionEvent.SHIFT_MASK) == 0 ? -90 : -1;
						upRotation.rotX(Math.toRadians(deltaAngle));
						upRotation.mul(oldTransform);
						updateModelRotation(upRotation);
						verticalAngle = (verticalAngle + deltaAngle) % 360;
						orientationToolTip.setTipText(String.format(angleTooltipFormat, verticalAngle));
						horizontalAngle = 0;
					}
				});
		this.turnDownButton = new AutoRepeatButton(
				new ResourceAction(preferences, ImportedFurnitureWizardStepsPanel.class, "TURN_DOWN", true)
				{
					@Override
					public void actionPerformed(ActionEvent ev)
					{
						Transform3D oldTransform = getModelRotationTransform();
						Transform3D downRotation = new Transform3D();
						int deltaAngle = (ev.getModifiers() & ActionEvent.SHIFT_MASK) == 0 ? 90 : 1;
						downRotation.rotX(Math.toRadians(deltaAngle));
						downRotation.mul(oldTransform);
						updateModelRotation(downRotation);
						verticalAngle = (verticalAngle + deltaAngle) % 360;
						orientationToolTip.setTipText(String.format(angleTooltipFormat, verticalAngle));
						horizontalAngle = 0;
					}
				});
		
		this.backFaceShownLabel = new JLabel(
				preferences.getLocalizedString(ImportedFurnitureWizardStepsPanel.class, "backFaceShownLabel.text"));
		this.backFaceShownCheckBox = new JCheckBox(SwingTools.getLocalizedLabelText(preferences,
				ImportedFurnitureWizardStepsPanel.class, "backFaceShownCheckBox.text"));
		this.backFaceShownCheckBox.addItemListener(new ItemListener()
		{
			public void itemStateChanged(ItemEvent ev)
			{
				controller.setBackFaceShown(backFaceShownCheckBox.isSelected());
			}
		});
		controller.addPropertyChangeListener(ImportedFurnitureWizardController.Property.BACK_FACE_SHOWN,
				new PropertyChangeListener()
				{
					public void propertyChange(PropertyChangeEvent ev)
					{
						// If back face shown changes update back face shown check box
						backFaceShownCheckBox.setSelected(controller.isBackFaceShown());
					}
				});
		this.rotationPreviewComponent = new RotationPreviewComponent(preferences, controller);
		
		// Attributes panel components
		this.attributesLabel = new JLabel(
				preferences.getLocalizedString(ImportedFurnitureWizardStepsPanel.class, "attributesLabel.text"));
		this.nameLabel = new JLabel(SwingTools.getLocalizedLabelText(preferences,
				ImportedFurnitureWizardStepsPanel.class, "nameLabel.text"));
		this.nameTextField = new JTextField(10);
		if (!OperatingSystem.isMacOSXLeopardOrSuperior())
		{
			SwingTools.addAutoSelectionOnFocusGain(this.nameTextField);
		}
		DocumentListener nameListener = new DocumentListener()
		{
			public void changedUpdate(DocumentEvent ev)
			{
				nameTextField.getDocument().removeDocumentListener(this);
				controller.setName(nameTextField.getText().trim());
				nameTextField.getDocument().addDocumentListener(this);
			}
			
			public void insertUpdate(DocumentEvent ev)
			{
				changedUpdate(ev);
			}
			
			public void removeUpdate(DocumentEvent ev)
			{
				changedUpdate(ev);
			}
		};
		this.nameTextField.getDocument().addDocumentListener(nameListener);
		controller.addPropertyChangeListener(ImportedFurnitureWizardController.Property.NAME,
				new PropertyChangeListener()
				{
					public void propertyChange(PropertyChangeEvent ev)
					{
						// If name changes update name text field
						if (!nameTextField.getText().trim().equals(controller.getName()))
						{
							nameTextField.setText(controller.getName());
						}
					}
				});
		
		this.addToCatalogCheckBox = new JCheckBox(SwingTools.getLocalizedLabelText(preferences,
				ImportedFurnitureWizardStepsPanel.class, "addToCatalogCheckBox.text"));
		// Propose the add to catalog option only for home furniture import
		this.addToCatalogCheckBox.setVisible(importHomePiece);
		this.addToCatalogCheckBox.addItemListener(new ItemListener()
		{
			public void itemStateChanged(ItemEvent ev)
			{
				if (addToCatalogCheckBox.isSelected())
				{
					categoryComboBox.setEnabled(true);
					controller.setCategory((FurnitureCategory) categoryComboBox.getSelectedItem());
				}
				else
				{
					categoryComboBox.setEnabled(false);
					controller.setCategory(null);
				}
			}
		});
		this.categoryLabel = new JLabel(SwingTools.getLocalizedLabelText(preferences,
				ImportedFurnitureWizardStepsPanel.class, "categoryLabel.text"));
		this.categoryComboBox = new JComboBox(preferences.getFurnitureCatalog().getCategories().toArray());
		// The piece category isn't enabled by default for home furniture import
		this.categoryComboBox.setEnabled(!importHomePiece);
		this.categoryComboBox.setEditable(true);
		final ComboBoxEditor defaultEditor = this.categoryComboBox.getEditor();
		// Change editor to edit category name
		this.categoryComboBox.setEditor(new ComboBoxEditor()
		{
			public Object getItem()
			{
				String name = (String) defaultEditor.getItem();
				name = name.trim();
				// If category is empty, replace it by the last selected item
				if (name.length() == 0)
				{
					Object selectedItem = categoryComboBox.getSelectedItem();
					setItem(selectedItem);
					return selectedItem;
				}
				else
				{
					FurnitureCategory category = new FurnitureCategory(name);
					// Search an existing category
					List<FurnitureCategory> categories = preferences.getFurnitureCatalog().getCategories();
					int categoryIndex = Collections.binarySearch(categories, category);
					if (categoryIndex >= 0)
					{
						return categories.get(categoryIndex);
					}
					// If no existing category was found, return a new one          
					return category;
				}
			}
			
			public void setItem(Object value)
			{
				if (value != null)
				{
					FurnitureCategory category = (FurnitureCategory) value;
					defaultEditor.setItem(category.getName());
				}
			}
			
			public void addActionListener(ActionListener l)
			{
				defaultEditor.addActionListener(l);
			}
			
			public Component getEditorComponent()
			{
				return defaultEditor.getEditorComponent();
			}
			
			public void removeActionListener(ActionListener l)
			{
				defaultEditor.removeActionListener(l);
			}
			
			public void selectAll()
			{
				defaultEditor.selectAll();
			}
		});
		this.categoryComboBox.setRenderer(new DefaultListCellRenderer()
		{
			public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected,
					boolean cellHasFocus)
			{
				FurnitureCategory category = (FurnitureCategory) value;
				return super.getListCellRendererComponent(list, category.getName(), index, isSelected, cellHasFocus);
			}
		});
		this.categoryComboBox.addItemListener(new ItemListener()
		{
			public void itemStateChanged(ItemEvent ev)
			{
				controller.setCategory((FurnitureCategory) ev.getItem());
			}
		});
		controller.addPropertyChangeListener(ImportedFurnitureWizardController.Property.CATEGORY,
				new PropertyChangeListener()
				{
					public void propertyChange(PropertyChangeEvent ev)
					{
						// If category changes update category combo box
						FurnitureCategory category = controller.getCategory();
						if (category != null)
						{
							categoryComboBox.setSelectedItem(category);
						}
					}
				});
		if (this.categoryComboBox.getItemCount() > 0)
		{
			this.categoryComboBox.setSelectedIndex(0);
		}
		
		this.widthLabel = new JLabel(SwingTools.getLocalizedLabelText(preferences,
				ImportedFurnitureWizardStepsPanel.class, "widthLabel.text", unitName));
		final float minimumLength = preferences.getLengthUnit().getMinimumLength();
		final float maximumLength = preferences.getLengthUnit().getMaximumLength();
		final NullableSpinner.NullableSpinnerLengthModel widthSpinnerModel = new NullableSpinner.NullableSpinnerLengthModel(
				preferences, Math.min(controller.getWidth(), minimumLength), maximumLength);
		this.widthSpinner = new NullableSpinner(widthSpinnerModel);
		widthSpinnerModel.addChangeListener(new ChangeListener()
		{
			public void stateChanged(ChangeEvent ev)
			{
				widthSpinnerModel.removeChangeListener(this);
				// If width spinner value changes update controller
				controller.setWidth(widthSpinnerModel.getLength());
				widthSpinnerModel.addChangeListener(this);
			}
		});
		controller.addPropertyChangeListener(ImportedFurnitureWizardController.Property.WIDTH,
				new PropertyChangeListener()
				{
					public void propertyChange(PropertyChangeEvent ev)
					{
						// If width changes update width spinner
						widthSpinnerModel.setLength(controller.getWidth());
						widthSpinnerModel.setMinimumLength(Math.min(controller.getWidth(), minimumLength));
					}
				});
		
		this.depthLabel = new JLabel(SwingTools.getLocalizedLabelText(preferences,
				ImportedFurnitureWizardStepsPanel.class, "depthLabel.text", unitName));
		final NullableSpinner.NullableSpinnerLengthModel depthSpinnerModel = new NullableSpinner.NullableSpinnerLengthModel(
				preferences, Math.min(controller.getDepth(), minimumLength), maximumLength);
		this.depthSpinner = new NullableSpinner(depthSpinnerModel);
		depthSpinnerModel.addChangeListener(new ChangeListener()
		{
			public void stateChanged(ChangeEvent ev)
			{
				depthSpinnerModel.removeChangeListener(this);
				// If depth spinner value changes update controller
				controller.setDepth(depthSpinnerModel.getLength());
				depthSpinnerModel.addChangeListener(this);
			}
		});
		controller.addPropertyChangeListener(ImportedFurnitureWizardController.Property.DEPTH,
				new PropertyChangeListener()
				{
					public void propertyChange(PropertyChangeEvent ev)
					{
						// If depth changes update depth spinner
						depthSpinnerModel.setLength(controller.getDepth());
						depthSpinnerModel.setMinimumLength(Math.min(controller.getDepth(), minimumLength));
					}
				});
		
		this.heightLabel = new JLabel(SwingTools.getLocalizedLabelText(preferences,
				ImportedFurnitureWizardStepsPanel.class, "heightLabel.text", unitName));
		final NullableSpinner.NullableSpinnerLengthModel heightSpinnerModel = new NullableSpinner.NullableSpinnerLengthModel(
				preferences, Math.min(controller.getHeight(), minimumLength), maximumLength);
		this.heightSpinner = new NullableSpinner(heightSpinnerModel);
		heightSpinnerModel.addChangeListener(new ChangeListener()
		{
			public void stateChanged(ChangeEvent ev)
			{
				heightSpinnerModel.removeChangeListener(this);
				// If width spinner value changes update controller
				controller.setHeight(heightSpinnerModel.getLength());
				heightSpinnerModel.addChangeListener(this);
			}
		});
		controller.addPropertyChangeListener(ImportedFurnitureWizardController.Property.HEIGHT,
				new PropertyChangeListener()
				{
					public void propertyChange(PropertyChangeEvent ev)
					{
						// If height changes update height spinner
						heightSpinnerModel.setLength(controller.getHeight());
						heightSpinnerModel.setMinimumLength(Math.min(controller.getHeight(), minimumLength));
					}
				});
		this.keepProportionsCheckBox = new JCheckBox(SwingTools.getLocalizedLabelText(preferences,
				ImportedFurnitureWizardStepsPanel.class, "keepProportionsCheckBox.text"));
		this.keepProportionsCheckBox.addItemListener(new ItemListener()
		{
			public void itemStateChanged(ItemEvent ev)
			{
				controller.setProportional(keepProportionsCheckBox.isSelected());
			}
		});
		controller.addPropertyChangeListener(ImportedFurnitureWizardController.Property.PROPORTIONAL,
				new PropertyChangeListener()
				{
					public void propertyChange(PropertyChangeEvent ev)
					{
						// If proportional property changes update keep proportions check box
						keepProportionsCheckBox.setSelected(controller.isProportional());
					}
				});
		
		this.elevationLabel = new JLabel(SwingTools.getLocalizedLabelText(preferences,
				ImportedFurnitureWizardStepsPanel.class, "elevationLabel.text", unitName));
		final NullableSpinner.NullableSpinnerLengthModel elevationSpinnerModel = new NullableSpinner.NullableSpinnerLengthModel(
				preferences, 0f, preferences.getLengthUnit().getMaximumElevation());
		this.elevationSpinner = new NullableSpinner(elevationSpinnerModel);
		elevationSpinnerModel.addChangeListener(new ChangeListener()
		{
			public void stateChanged(ChangeEvent ev)
			{
				elevationSpinnerModel.removeChangeListener(this);
				controller.setElevation(elevationSpinnerModel.getLength());
				elevationSpinnerModel.addChangeListener(this);
			}
		});
		controller.addPropertyChangeListener(ImportedFurnitureWizardController.Property.ELEVATION,
				new PropertyChangeListener()
				{
					public void propertyChange(PropertyChangeEvent ev)
					{
						// If elevation changes update elevation spinner
						elevationSpinnerModel.setLength(controller.getElevation());
					}
				});
		
		this.movableCheckBox = new JCheckBox(SwingTools.getLocalizedLabelText(preferences,
				ImportedFurnitureWizardStepsPanel.class, "movableCheckBox.text"));
		this.movableCheckBox.addItemListener(new ItemListener()
		{
			public void itemStateChanged(ItemEvent ev)
			{
				controller.setMovable(movableCheckBox.isSelected());
			}
		});
		controller.addPropertyChangeListener(ImportedFurnitureWizardController.Property.MOVABLE,
				new PropertyChangeListener()
				{
					public void propertyChange(PropertyChangeEvent ev)
					{
						// If movable changes update movable check box
						movableCheckBox.setSelected(controller.isMovable());
					}
				});
		
		this.doorOrWindowCheckBox = new JCheckBox(SwingTools.getLocalizedLabelText(preferences,
				ImportedFurnitureWizardStepsPanel.class, "doorOrWindowCheckBox.text"));
		this.doorOrWindowCheckBox.addItemListener(new ItemListener()
		{
			public void itemStateChanged(ItemEvent ev)
			{
				controller.setDoorOrWindow(doorOrWindowCheckBox.isSelected());
			}
		});
		controller.addPropertyChangeListener(ImportedFurnitureWizardController.Property.DOOR_OR_WINDOW,
				new PropertyChangeListener()
				{
					public void propertyChange(PropertyChangeEvent ev)
					{
						// If door or window changes update door or window check box
						boolean doorOrWindow = controller.isDoorOrWindow();
						doorOrWindowCheckBox.setSelected(doorOrWindow);
						movableCheckBox.setEnabled(!doorOrWindow && controller.getStaircaseCutOutShape() == null);
					}
				});
		
		this.staircaseCheckBox = new JCheckBox(SwingTools.getLocalizedLabelText(preferences,
				ImportedFurnitureWizardStepsPanel.class, "staircaseCheckBox.text"));
		this.staircaseCheckBox.addItemListener(new ItemListener()
		{
			public void itemStateChanged(ItemEvent ev)
			{
				controller.setStaircaseCutOutShape(staircaseCheckBox.isSelected() ? "M0,0 v1 h1 v-1 z" : null);
			}
		});
		controller.addPropertyChangeListener(ImportedFurnitureWizardController.Property.STAIRCASE_CUT_OUT_SHAPE,
				new PropertyChangeListener()
				{
					public void propertyChange(PropertyChangeEvent ev)
					{
						// If staircase cut out shape changes update its check box
						String staircaseCutOutShape = controller.getStaircaseCutOutShape();
						staircaseCheckBox.setSelected(staircaseCutOutShape != null);
						movableCheckBox.setEnabled(!controller.isDoorOrWindow() && staircaseCutOutShape == null);
					}
				});
		
		this.colorLabel = new JLabel(String.format(SwingTools.getLocalizedLabelText(preferences,
				ImportedFurnitureWizardStepsPanel.class, "colorLabel.text"), unitName));
		this.colorButton = new ColorButton(preferences);
		this.colorButton.setColorDialogTitle(
				preferences.getLocalizedString(ImportedFurnitureWizardStepsPanel.class, "colorDialog.title"));
		this.colorButton.addPropertyChangeListener(ColorButton.COLOR_PROPERTY, new PropertyChangeListener()
		{
			public void propertyChange(PropertyChangeEvent ev)
			{
				controller.setColor(colorButton.getColor());
			}
		});
		this.clearColorButton = new JButton(SwingTools.getLocalizedLabelText(preferences,
				ImportedFurnitureWizardStepsPanel.class, "clearColorButton.text"));
		this.clearColorButton.addActionListener(new ActionListener()
		{
			public void actionPerformed(ActionEvent ev)
			{
				controller.setColor(null);
			}
		});
		this.clearColorButton.setEnabled(false);
		controller.addPropertyChangeListener(ImportedFurnitureWizardController.Property.COLOR,
				new PropertyChangeListener()
				{
					public void propertyChange(PropertyChangeEvent ev)
					{
						// If color changes update color buttons
						colorButton.setColor(controller.getColor());
						clearColorButton.setEnabled(controller.getColor() != null);
					}
				});
		
		this.attributesPreviewComponent = new AttributesPreviewComponent(controller);
		
		// Icon panel components
		this.iconLabel = new JLabel(
				preferences.getLocalizedString(ImportedFurnitureWizardStepsPanel.class, "iconLabel.text"));
		this.iconPreviewComponent = new IconPreviewComponent(this.controller);
	}
	
	/**
	 * A button that repeats its action when kept pressed.
	 */
	private class AutoRepeatButton extends JButton
	{
		private boolean shiftPressed;
		
		public AutoRepeatButton(final Action action)
		{
			super(action);
			// Create a timer that will repeat action each 40 ms when SHIFT is pressed
			final Timer timer = new Timer(40, new ActionListener()
			{
				public void actionPerformed(final ActionEvent ev)
				{
					action.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, null, ev.getWhen(),
							ActionEvent.SHIFT_MASK));
					showOrientationToolTip();
				}
			});
			timer.setInitialDelay(250);
			
			// Update timer when button is armed
			addChangeListener(new ChangeListener()
			{
				public void stateChanged(ChangeEvent ev)
				{
					EventQueue.invokeLater(new Runnable()
					{
						public void run()
						{
							if (shiftPressed)
							{
								if (getModel().isArmed() && !timer.isRunning())
								{
									timer.restart();
								}
								else if (!getModel().isArmed() && timer.isRunning())
								{
									timer.stop();
								}
							}
						}
					});
				}
			});
			addMouseListener(new MouseAdapter()
			{
				@Override
				public void mousePressed(final MouseEvent ev)
				{
					shiftPressed = ev.isShiftDown();
				}
				
				@Override
				public void mouseClicked(final MouseEvent ev)
				{
					showOrientationToolTip();
				}
				
				@Override
				public void mouseReleased(MouseEvent ev)
				{
					new Timer(500, new ActionListener()
					{
						public void actionPerformed(final ActionEvent ev)
						{
							deleteOrientationToolTip();
							((Timer) ev.getSource()).stop();
						}
					}).start();
				}
			});
		}
	}
	
	/**
	 * Shows the orientation tool tip.
	 */
	private void showOrientationToolTip()
	{
		if (this.orientationToolTipWindow == null)
		{
			// Show tool tip in a window (we don't use a Swing Popup because 
			// we require the tool tip window to resize itself depending on the content)
			this.orientationToolTipWindow = new JWindow(SwingUtilities.getWindowAncestor(this));
			this.orientationToolTipWindow.setFocusableWindowState(false);
			this.orientationToolTipWindow.add(this.orientationToolTip);
		}
		else
		{
			this.orientationToolTip.revalidate();
		}
		Point point = MouseInfo.getPointerInfo().getLocation();
		// Add to point the half of cursor size
		Dimension cursorSize = getToolkit().getBestCursorSize(16, 16);
		if (cursorSize.width != 0)
		{
			point.x += cursorSize.width + 2;
			point.y += cursorSize.height + 2;
		}
		else
		{
			// If custom cursor isn't supported let's consider 
			// default cursor size is 16 pixels wide
			point.x += 18;
			point.y += 18;
		}
		this.orientationToolTipWindow.setLocation(point);
		this.orientationToolTipWindow.pack();
		this.orientationToolTipWindow.setVisible(true);
		this.orientationToolTip.paintImmediately(this.orientationToolTip.getBounds());
	}
	
	/**
	 * Deletes tool tip text window from screen. 
	 */
	private void deleteOrientationToolTip()
	{
		if (this.orientationToolTipWindow != null)
		{
			this.orientationToolTipWindow.setVisible(false);
		}
	}
	
	/**
	 * Sets components mnemonics and label / component associations.
	 */
	private void setMnemonics(UserPreferences preferences)
	{
		if (!OperatingSystem.isMacOSX())
		{
			if (this.findModelsButton != null)
			{
				this.findModelsButton
						.setMnemonic(
								KeyStroke
										.getKeyStroke(preferences.getLocalizedString(
												ImportedFurnitureWizardStepsPanel.class, "findModelsButton.mnemonic"))
										.getKeyCode());
			}
			this.backFaceShownCheckBox
					.setMnemonic(
							KeyStroke
									.getKeyStroke(preferences.getLocalizedString(
											ImportedFurnitureWizardStepsPanel.class, "backFaceShownCheckBox.mnemonic"))
									.getKeyCode());
			this.nameLabel
					.setDisplayedMnemonic(
							KeyStroke
									.getKeyStroke(preferences.getLocalizedString(
											ImportedFurnitureWizardStepsPanel.class, "nameLabel.mnemonic"))
									.getKeyCode());
			this.nameLabel.setLabelFor(this.nameTextField);
			this.categoryLabel
					.setDisplayedMnemonic(
							KeyStroke
									.getKeyStroke(preferences.getLocalizedString(
											ImportedFurnitureWizardStepsPanel.class, "categoryLabel.mnemonic"))
									.getKeyCode());
			this.categoryLabel.setLabelFor(this.categoryComboBox);
			this.addToCatalogCheckBox
					.setMnemonic(
							KeyStroke
									.getKeyStroke(preferences.getLocalizedString(
											ImportedFurnitureWizardStepsPanel.class, "addToCatalogCheckBox.mnemonic"))
									.getKeyCode());
			;
			this.widthLabel
					.setDisplayedMnemonic(
							KeyStroke
									.getKeyStroke(preferences.getLocalizedString(
											ImportedFurnitureWizardStepsPanel.class, "widthLabel.mnemonic"))
									.getKeyCode());
			this.widthLabel.setLabelFor(this.widthSpinner);
			this.depthLabel
					.setDisplayedMnemonic(
							KeyStroke
									.getKeyStroke(preferences.getLocalizedString(
											ImportedFurnitureWizardStepsPanel.class, "depthLabel.mnemonic"))
									.getKeyCode());
			this.depthLabel.setLabelFor(this.depthSpinner);
			this.heightLabel
					.setDisplayedMnemonic(
							KeyStroke
									.getKeyStroke(preferences.getLocalizedString(
											ImportedFurnitureWizardStepsPanel.class, "heightLabel.mnemonic"))
									.getKeyCode());
			this.heightLabel.setLabelFor(this.heightSpinner);
			this.keepProportionsCheckBox.setMnemonic(
					KeyStroke.getKeyStroke(preferences.getLocalizedString(ImportedFurnitureWizardStepsPanel.class,
							"keepProportionsCheckBox.mnemonic")).getKeyCode());
			this.elevationLabel
					.setDisplayedMnemonic(
							KeyStroke
									.getKeyStroke(preferences.getLocalizedString(
											ImportedFurnitureWizardStepsPanel.class, "elevationLabel.mnemonic"))
									.getKeyCode());
			this.elevationLabel.setLabelFor(this.elevationSpinner);
			this.movableCheckBox
					.setMnemonic(
							KeyStroke
									.getKeyStroke(preferences.getLocalizedString(
											ImportedFurnitureWizardStepsPanel.class, "movableCheckBox.mnemonic"))
									.getKeyCode());
			;
			this.doorOrWindowCheckBox
					.setMnemonic(
							KeyStroke
									.getKeyStroke(preferences.getLocalizedString(
											ImportedFurnitureWizardStepsPanel.class, "doorOrWindowCheckBox.mnemonic"))
									.getKeyCode());
			;
			this.staircaseCheckBox
					.setMnemonic(
							KeyStroke
									.getKeyStroke(preferences.getLocalizedString(
											ImportedFurnitureWizardStepsPanel.class, "staircaseCheckBox.mnemonic"))
									.getKeyCode());
			;
			this.colorLabel
					.setDisplayedMnemonic(
							KeyStroke
									.getKeyStroke(preferences.getLocalizedString(
											ImportedFurnitureWizardStepsPanel.class, "colorLabel.mnemonic"))
									.getKeyCode());
			this.colorLabel.setLabelFor(this.colorButton);
			this.clearColorButton
					.setMnemonic(
							KeyStroke
									.getKeyStroke(preferences.getLocalizedString(
											ImportedFurnitureWizardStepsPanel.class, "clearColorButton.mnemonic"))
									.getKeyCode());
		}
	}
	
	/**
	 * Layouts components in 4 panels added to this panel as cards. 
	 */
	private void layoutComponents()
	{
		this.cardLayout = new CardLayout();
		setLayout(this.cardLayout);
		
		JPanel modelPanel = new JPanel(new GridBagLayout());
		modelPanel.add(this.modelChoiceOrChangeLabel, new GridBagConstraints(0, 0, 2, 1, 0, 0,
				GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(5, 0, 5, 0), 0, 0));
		if (this.findModelsButton != null)
		{
			modelPanel.add(this.modelChoiceOrChangeButton, new GridBagConstraints(0, 1, 1, 1, 1, 0,
					GridBagConstraints.LINE_END, GridBagConstraints.NONE, new Insets(0, 0, 0, 10), 0, 0));
			modelPanel.add(this.findModelsButton, new GridBagConstraints(1, 1, 1, 1, 1, 0,
					GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
		}
		else
		{
			modelPanel.add(this.modelChoiceOrChangeButton, new GridBagConstraints(0, 1, 2, 1, 1, 0,
					GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
		}
		modelPanel.add(this.modelChoiceErrorLabel, new GridBagConstraints(0, 2, 2, 1, 0, 0,
				GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(5, 0, 0, 0), 0, 0));
		modelPanel.add(this.modelPreviewComponent, new GridBagConstraints(0, 3, 2, 1, 0, 1, GridBagConstraints.CENTER,
				GridBagConstraints.NONE, new Insets(5, 0, 5, 0), 0, 0));
		
		JPanel orientationPanel = new JPanel(new GridBagLayout());
		orientationPanel.add(this.orientationLabel, new GridBagConstraints(0, 0, 2, 1, 0, 0,
				GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(5, 0, 5, 0), 0, 0));
		orientationPanel.add(this.rotationPreviewComponent, new GridBagConstraints(0, 1, 1, 1, 1, 1,
				GridBagConstraints.LINE_END, GridBagConstraints.NONE, new Insets(0, 0, 5, 15), 0, 0));
		JPanel rotationButtonsPanel = new JPanel(new GridBagLayout())
		{
			@Override
			public void applyComponentOrientation(ComponentOrientation orientation)
			{
				// Ignore panel orientation to ensure left button is always at left of panel
			}
		};
		if (!OperatingSystem.isMacOSX())
		{
			// Make buttons square
			Dimension preferredSize = this.turnUpButton.getPreferredSize();
			preferredSize.width = preferredSize.height = preferredSize.height + 4;
			this.turnUpButton.setPreferredSize(preferredSize);
			this.turnLeftButton.setPreferredSize(preferredSize);
			this.turnRightButton.setPreferredSize(preferredSize);
			this.turnDownButton.setPreferredSize(preferredSize);
			this.defaultOrientationButton.setPreferredSize(preferredSize);
		}
		
		rotationButtonsPanel.add(this.turnUpButton, new GridBagConstraints(1, 0, 1, 1, 0, 0, GridBagConstraints.SOUTH,
				GridBagConstraints.NONE, new Insets(0, 0, 5, 0), 0, 0));
		rotationButtonsPanel.add(this.turnLeftButton, new GridBagConstraints(0, 1, 1, 1, 0, 0, GridBagConstraints.EAST,
				GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));
		rotationButtonsPanel.add(this.defaultOrientationButton, new GridBagConstraints(1, 1, 1, 1, 0, 0,
				GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0, 0, 5, 0), 0, 0));
		rotationButtonsPanel.add(this.turnRightButton, new GridBagConstraints(2, 1, 1, 1, 1, 0, GridBagConstraints.WEST,
				GridBagConstraints.NONE, new Insets(0, 5, 5, 0), 0, 0));
		rotationButtonsPanel.add(this.turnDownButton, new GridBagConstraints(1, 2, 1, 1, 0, 0, GridBagConstraints.NORTH,
				GridBagConstraints.NONE, new Insets(0, 0, 5, 0), 0, 0));
		orientationPanel.add(rotationButtonsPanel, new GridBagConstraints(1, 1, 1, 1, 1, 1,
				GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 0, 5, 0), 0, 0));
		orientationPanel.add(this.backFaceShownLabel, new GridBagConstraints(0, 4, 2, 1, 0, 0,
				GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(5, 0, 5, 0), 0, 0));
		orientationPanel.add(this.backFaceShownCheckBox, new GridBagConstraints(0, 5, 2, 1, 0, 0,
				GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0, 0, 5, 0), 0, 0));
		
		JPanel attributesPanel = new JPanel(new GridBagLayout());
		attributesPanel.add(this.attributesLabel, new GridBagConstraints(0, 0, 3, 1, 0, 0,
				GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(5, 0, 5, 0), 0, 0));
		attributesPanel.add(this.attributesPreviewComponent, new GridBagConstraints(0, 1, 1, 13, 1, 0,
				GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));
		attributesPanel.add(this.nameLabel, new GridBagConstraints(1, 1, 1, 1, 0, 0, GridBagConstraints.LINE_START,
				GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));
		attributesPanel.add(this.nameTextField, new GridBagConstraints(2, 1, 1, 1, 0, 0, GridBagConstraints.CENTER,
				GridBagConstraints.HORIZONTAL, new Insets(0, 0, 5, 0), 0, 0));
		attributesPanel.add(this.addToCatalogCheckBox, new GridBagConstraints(1, 2, 2, 1, 0, 0,
				GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 0, 5, 0), 0, 0));
		attributesPanel.add(this.categoryLabel, new GridBagConstraints(1, 3, 1, 1, 0, 0, GridBagConstraints.LINE_START,
				GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));
		attributesPanel.add(this.categoryComboBox, new GridBagConstraints(2, 3, 1, 1, 0, 0, GridBagConstraints.CENTER,
				GridBagConstraints.HORIZONTAL, new Insets(0, 0, 5, 0), 0, 0));
		attributesPanel.add(this.widthLabel, new GridBagConstraints(1, 4, 1, 1, 0, 0, GridBagConstraints.LINE_START,
				GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));
		attributesPanel.add(this.widthSpinner, new GridBagConstraints(2, 4, 1, 1, 0, 0, GridBagConstraints.CENTER,
				GridBagConstraints.HORIZONTAL, new Insets(0, 0, 5, 0), 0, 0));
		attributesPanel.add(this.depthLabel, new GridBagConstraints(1, 5, 1, 1, 0, 0, GridBagConstraints.LINE_START,
				GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));
		attributesPanel.add(this.depthSpinner, new GridBagConstraints(2, 5, 1, 1, 0, 0, GridBagConstraints.CENTER,
				GridBagConstraints.HORIZONTAL, new Insets(0, 0, 5, 0), 0, 0));
		attributesPanel.add(this.heightLabel, new GridBagConstraints(1, 6, 1, 1, 0, 0, GridBagConstraints.LINE_START,
				GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));
		attributesPanel.add(this.heightSpinner, new GridBagConstraints(2, 6, 1, 1, 0, 0, GridBagConstraints.CENTER,
				GridBagConstraints.HORIZONTAL, new Insets(0, 0, 5, 0), 0, 0));
		attributesPanel.add(this.keepProportionsCheckBox, new GridBagConstraints(1, 7, 2, 1, 0, 0,
				GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 0, 10, 0), 0, 0));
		attributesPanel.add(this.elevationLabel, new GridBagConstraints(1, 8, 1, 1, 0, 0, GridBagConstraints.LINE_START,
				GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));
		attributesPanel.add(this.elevationSpinner, new GridBagConstraints(2, 8, 1, 1, 0, 0, GridBagConstraints.CENTER,
				GridBagConstraints.HORIZONTAL, new Insets(0, 0, 5, 0), 0, 0));
		attributesPanel.add(this.movableCheckBox, new GridBagConstraints(1, 9, 2, 1, 0, 0,
				GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 0, 5, 0), 0, 0));
		attributesPanel.add(this.doorOrWindowCheckBox, new GridBagConstraints(1, 10, 2, 1, 0, 0,
				GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 0, 5, 0), 0, 0));
		attributesPanel.add(this.staircaseCheckBox, new GridBagConstraints(1, 11, 2, 1, 0, 0,
				GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 0, 5, 0), 0, 0));
		attributesPanel.add(this.colorLabel, new GridBagConstraints(1, 12, 1, 1, 0, 0, GridBagConstraints.LINE_START,
				GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));
		attributesPanel.add(this.colorButton, new GridBagConstraints(2, 12, 1, 1, 0, 0, GridBagConstraints.CENTER,
				GridBagConstraints.HORIZONTAL, new Insets(0, 0, 5, 0), 0, 0));
		attributesPanel.add(this.clearColorButton, new GridBagConstraints(2, 13, 1, 1, 0, 0, GridBagConstraints.CENTER,
				GridBagConstraints.HORIZONTAL, new Insets(0, 0, 5, 0), 0, 0));
		// Add a dummy label to force components to be at top of panel
		attributesPanel.add(new JLabel(), new GridBagConstraints(1, 14, 1, 1, 1, 1, GridBagConstraints.CENTER,
				GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
		
		JPanel iconPanel = new JPanel(new GridBagLayout());
		iconPanel.add(this.iconLabel, new GridBagConstraints(0, 0, 1, 1, 0, 0, GridBagConstraints.LINE_START,
				GridBagConstraints.NONE, new Insets(5, 0, 5, 0), 0, 0));
		iconPanel.add(this.iconPreviewComponent, new GridBagConstraints(0, 1, 1, 1, 1, 1, GridBagConstraints.CENTER,
				GridBagConstraints.NONE, new Insets(5, 0, 5, 0), 0, 0));
		
		add(modelPanel, ImportedFurnitureWizardController.Step.MODEL.name());
		add(orientationPanel, ImportedFurnitureWizardController.Step.ROTATION.name());
		add(attributesPanel, ImportedFurnitureWizardController.Step.ATTRIBUTES.name());
		add(iconPanel, ImportedFurnitureWizardController.Step.ICON.name());
	}
	
	/**
	 * Switches to the component card matching current step.   
	 */
	private void updateStep(ImportedFurnitureWizardController controller)
	{
		ImportedFurnitureWizardController.Step step = controller.getStep();
		this.cardLayout.show(this, step.name());
		switch (step)
		{
			case MODEL:
				this.modelChoiceOrChangeButton.requestFocusInWindow();
				break;
			case ATTRIBUTES:
				this.nameTextField.requestFocusInWindow();
				break;
		}
	}
	
	/**
	 * Returns the transformation matching current model rotation.
	 */
	private Transform3D getModelRotationTransform()
	{
		float[][] modelRotation = this.controller.getModelRotation();
		Matrix3f modelRotationMatrix = new Matrix3f(modelRotation[0][0], modelRotation[0][1], modelRotation[0][2],
				modelRotation[1][0], modelRotation[1][1], modelRotation[1][2], modelRotation[2][0], modelRotation[2][1],
				modelRotation[2][2]);
		Transform3D transform = new Transform3D();
		transform.setRotation(modelRotationMatrix);
		return transform;
	}
	
	/**
	 * Updates model rotation from the values of <code>transform</code>.
	 */
	private void updateModelRotation(Transform3D transform)
	{
		Matrix3f modelRotationMatrix = new Matrix3f();
		transform.getRotationScale(modelRotationMatrix);
		this.controller.setModelRotation(
				new float[][] { { modelRotationMatrix.m00, modelRotationMatrix.m01, modelRotationMatrix.m02 },
						{ modelRotationMatrix.m10, modelRotationMatrix.m11, modelRotationMatrix.m12 },
						{ modelRotationMatrix.m20, modelRotationMatrix.m21, modelRotationMatrix.m22 } });
	}
	
	/**
	 * Updates controller initial values from <code>piece</code>. 
	 */
	private void updateController(final CatalogPieceOfFurniture piece, final UserPreferences preferences)
	{
		updatePreviewComponentsModel(null);
		if (piece == null)
		{
			setModelChoiceTexts(preferences);
		}
		else
		{
			setModelChangeTexts(preferences);
			setReadingState();
			// Load piece model asynchronously
			ModelManager.getInstance().loadModel(piece.getModel(), new ModelManager.ModelObserver()
			{
				public void modelUpdated(BranchGroup modelRoot)
				{
					updatePreviewComponentsModel(piece.getModel());
					setDefaultState();
					controller.setModel(piece.getModel());
					controller.setModelRotation(piece.getModelRotation());
					controller.setBackFaceShown(piece.isBackFaceShown());
					controller.setName(piece.getName());
					controller.setCategory(piece.getCategory());
					controller.setWidth(piece.getWidth());
					controller.setDepth(piece.getDepth());
					controller.setHeight(piece.getHeight());
					controller.setMovable(piece.isMovable());
					controller.setDoorOrWindow(piece.isDoorOrWindow());
					controller.setStaircaseCutOutShape(piece.getStaircaseCutOutShape());
					controller.setElevation(piece.getElevation());
					controller.setColor(piece.getColor());
					controller.setIconYaw(piece.getIconYaw());
					controller.setProportional(piece.isProportional());
				}
				
				public void modelError(Exception ex)
				{
					controller.setModel(null);
					setModelChoiceTexts(preferences);
					modelChoiceErrorLabel.setVisible(true);
					if (isShowing())
					{
						SwingUtilities.getWindowAncestor(modelChoiceErrorLabel).pack();
					}
					setDefaultState();
				}
			});
		}
	}
	
	/**
	 * Reads model from <code>modelName</code> and updates controller values.
	 */
	private void updateController(final String modelName, final UserPreferences preferences,
			final ContentManager contentManager, final FurnitureCategory defaultCategory, final boolean ignoreException)
	{
		// Cancel current model
		this.controller.setModel(null);
		updatePreviewComponentsModel(null);
		setReadingState();
		// Read model in modelLoader executor
		this.modelLoader.execute(new Runnable()
		{
			public void run()
			{
				Content modelContent = null;
				try
				{
					modelContent = contentManager.getContent(modelName);
				}
				catch (RecorderException ex)
				{
					setDefaultStateAndShowModelChoiceError(modelName, preferences, !ignoreException);
				}
				
				try
				{
					BranchGroup model = ModelManager.getInstance().loadModel(modelContent);
					final Vector3f modelSize = ModelManager.getInstance().getSize(model);
					// Copy model to a temporary OBJ content with materials and textures
					final Content copiedContent = copyToTemporaryOBJContent(model, modelName);
					EventQueue.invokeLater(new Runnable()
					{
						public void run()
						{
							// Load copied content using cache to make it accessible by preview components
							ModelManager.getInstance().loadModel(copiedContent, new ModelManager.ModelObserver()
							{
								public void modelUpdated(BranchGroup modelRoot)
								{
									setDefaultStateAndInitializeReadModel(copiedContent, modelName, defaultCategory,
											modelSize, preferences, contentManager);
								}
								
								public void modelError(Exception ex)
								{
									setDefaultStateAndShowModelChoiceError(modelName, preferences, !ignoreException);
								}
							});
						}
					});
					return;
				}
				catch (IllegalArgumentException ex)
				{
					// Thrown by getSize if model is empty
				}
				catch (IOException ex)
				{
					// Try with zipped content
				}
				
				try
				{
					// Copy model content to a temporary content
					modelContent = TemporaryURLContent.copyToTemporaryURLContent(modelContent);
				}
				catch (IOException ex)
				{
					setDefaultStateAndShowModelChoiceError(modelName, preferences, !ignoreException);
					return;
				}
				
				// If content couldn't be loaded, try to load model as a zipped file
				ZipInputStream zipIn = null;
				try
				{
					URLContent urlContent = (URLContent) modelContent;
					// Open zipped stream
					zipIn = new ZipInputStream(urlContent.openStream());
					// Parse entries to see if one is readable
					while (true)
					{
						ZipEntry entry;
						try
						{
							if ((entry = zipIn.getNextEntry()) == null)
							{
								// No more entry
								break;
							}
						}
						catch (IllegalArgumentException ex)
						{
							// Exception thrown if entry name can't be read
							break;
						}
						
						String entryName = entry.getName();
						// Ignore directory entries and entries starting by a dot
						if (!entryName.endsWith("/"))
						{
							int slashIndex = entryName.lastIndexOf('/');
							String entryFileName = entryName.substring(++slashIndex);
							if (!entryFileName.startsWith("."))
							{
								URL entryUrl = new URL("jar:" + urlContent.getURL() + "!/" + URLEncoder
										.encode(entryName, "UTF-8").replace("+", "%20").replace("%2F", "/"));
								final Content entryContent = new TemporaryURLContent(entryUrl);
								final AtomicReference<Vector3f> modelSize = new AtomicReference<Vector3f>();
								final CountDownLatch latch = new CountDownLatch(1);
								EventQueue.invokeAndWait(new Runnable()
								{
									public void run()
									{
										// Load content using cache to make it accessible by preview components
										ModelManager.getInstance().loadModel(entryContent,
												new ModelManager.ModelObserver()
												{
													public void modelUpdated(BranchGroup modelRoot)
													{
														try
														{
															modelSize
																	.set(ModelManager.getInstance().getSize(modelRoot));
														}
														catch (IllegalArgumentException ex)
														{
															// Thrown by getSize if model is empty                              
														}
														latch.countDown();
													}
													
													public void modelError(Exception ex)
													{
														latch.countDown();
													}
												});
									}
								});
								
								latch.await();
								if (modelSize.get() != null)
								{
									// Check if all remaining entries in the ZIP file can be read, to be able to save edited home with them
									do
									{
										try
										{
											entry = zipIn.getNextEntry();
										}
										catch (IllegalArgumentException ex)
										{
											// Exception thrown if entry name can't be read
											break;
										}
									} while (entry != null);
									
									if (entry == null)
									{
										EventQueue.invokeAndWait(new Runnable()
										{
											public void run()
											{
												setDefaultStateAndInitializeReadModel(entryContent, modelName,
														defaultCategory, modelSize.get(), preferences, contentManager);
											}
										});
										return;
									}
								}
							}
						}
					}
				}
				catch (IOException ex)
				{
					setDefaultStateAndShowModelChoiceError(modelName, preferences, !ignoreException);
					return;
				}
				catch (InterruptedException ex)
				{
					setDefaultState();
					return;
				}
				catch (InvocationTargetException ex)
				{
					// Show next message
				}
				finally
				{
					try
					{
						if (zipIn != null)
						{
							zipIn.close();
						}
					}
					catch (IOException ex)
					{
						// Ignore close exception
					}
				}
				
				// Found no readable model
				EventQueue.invokeLater(new Runnable()
				{
					public void run()
					{
						if (isShowing())
						{
							setDefaultState();
							setModelChoiceTexts(preferences);
							JOptionPane.showMessageDialog(
									SwingUtilities.getRootPane(ImportedFurnitureWizardStepsPanel.this),
									preferences.getLocalizedString(ImportedFurnitureWizardStepsPanel.class,
											"modelChoiceFormatError"));
						}
					}
				});
			}
		});
	}
	
	/**
	 * Restores default state and initializes read model.
	 */
	private void setDefaultStateAndInitializeReadModel(final Content modelContent, final String modelName,
			final FurnitureCategory defaultCategory, final Vector3f modelSize, final UserPreferences preferences,
			final ContentManager contentManager)
	{
		setDefaultState();
		updatePreviewComponentsModel(modelContent);
		controller.setModel(modelContent);
		setModelChangeTexts(preferences);
		modelChoiceErrorLabel.setVisible(false);
		controller.setModelRotation(new float[][] { { 1, 0, 0 }, { 0, 1, 0 }, { 0, 0, 1 } });
		controller.setBackFaceShown(false);
		controller.setName(contentManager.getPresentationName(modelName, ContentManager.ContentType.MODEL));
		controller.setCategory(defaultCategory);
		// Initialize size with default values
		controller.setWidth(modelSize.x);
		controller.setDepth(modelSize.z);
		controller.setHeight(modelSize.y);
		controller.setMovable(true);
		controller.setDoorOrWindow(false);
		controller.setStaircaseCutOutShape(null);
		controller.setColor(null);
		controller.setIconYaw((float) Math.PI / 8);
		controller.setProportional(true);
	}
	
	/**
	 * Restores default state and shows an error message about the chosen model.
	 */
	private void setDefaultStateAndShowModelChoiceError(final String modelName, final UserPreferences preferences,
			boolean showError)
	{
		setDefaultState();
		if (showError)
		{
			EventQueue.invokeLater(new Runnable()
			{
				public void run()
				{
					JOptionPane.showMessageDialog(SwingUtilities.getRootPane(ImportedFurnitureWizardStepsPanel.this),
							preferences.getLocalizedString(ImportedFurnitureWizardStepsPanel.class, "modelChoiceError",
									modelName));
				}
			});
		}
	}
	
	/**
	 * Returns a copy of a given <code>model</code> as a zip content at OBJ format.
	 * Caution : this method must be thread safe because it can be called from model loader executor. 
	 */
	private Content copyToTemporaryOBJContent(BranchGroup model, String modelName) throws IOException
	{
		try
		{
			setReadingState();
			String objFile = new File(modelName).getName();
			if (!objFile.toLowerCase().endsWith(".obj"))
			{
				objFile += ".obj";
			}
			// Ensure the file contains only letters, figures, underscores, dots, hyphens or spaces
			if (objFile.matches(".*[^a-zA-Z0-9_\\.\\-\\ ].*"))
			{
				objFile = "model.obj";
			}
			File tempZipFile = OperatingSystem.createTemporaryFile("import", ".zip");
			OBJWriter.writeNodeInZIPFile(model, tempZipFile, 0, objFile, "3D model import " + modelName);
			return new TemporaryURLContent(new URL("jar:" + tempZipFile.toURI().toURL() + "!/"
					+ URLEncoder.encode(objFile, "UTF-8").replace("+", "%20")));
		}
		finally
		{
			setDefaultState();
		}
	}
	
	/**
	 * Sets the cursor to wait cursor and disables model choice button.
	 */
	private void setReadingState()
	{
		this.modelChoiceOrChangeButton.setEnabled(false);
		Component rootPane = SwingUtilities.getRoot(ImportedFurnitureWizardStepsPanel.this);
		if (rootPane != null)
		{
			if (this.defaultCursor == null)
			{
				this.defaultCursor = rootPane.getCursor();
			}
			rootPane.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
		}
		else
		{
			addAncestorListener(new AncestorListener()
			{
				public void ancestorAdded(AncestorEvent event)
				{
					removeAncestorListener(this);
					if (!modelChoiceOrChangeButton.isEnabled())
					{
						setReadingState();
					}
				}
				
				public void ancestorRemoved(AncestorEvent event)
				{}
				
				public void ancestorMoved(AncestorEvent event)
				{}
			});
		}
	}
	
	/**
	 * Sets the default cursor and enables model choice button.
	 */
	private void setDefaultState()
	{
		if (EventQueue.isDispatchThread())
		{
			this.modelChoiceOrChangeButton.setEnabled(true);
			Component rootPane = SwingUtilities.getRoot(ImportedFurnitureWizardStepsPanel.this);
			if (rootPane != null)
			{
				rootPane.setCursor(this.defaultCursor);
			}
		}
		else
		{
			EventQueue.invokeLater(new Runnable()
			{
				public void run()
				{
					setDefaultState();
				}
			});
		}
	}
	
	/**
	 * Updates the model displayed by preview components.  
	 */
	private void updatePreviewComponentsModel(final Content model)
	{
		modelPreviewComponent.setModel(model);
		rotationPreviewComponent.setModel(model);
		attributesPreviewComponent.setModel(model);
		iconPreviewComponent.setModel(model);
	}
	
	/**
	 * Sets the texts of label and button of model panel with change texts. 
	 */
	private void setModelChangeTexts(UserPreferences preferences)
	{
		this.modelChoiceOrChangeLabel.setText(
				preferences.getLocalizedString(ImportedFurnitureWizardStepsPanel.class, "modelChangeLabel.text"));
		this.modelChoiceOrChangeButton.setText(SwingTools.getLocalizedLabelText(preferences,
				ImportedFurnitureWizardStepsPanel.class, "modelChangeButton.text"));
		if (!OperatingSystem.isMacOSX())
		{
			this.modelChoiceOrChangeButton
					.setMnemonic(
							KeyStroke
									.getKeyStroke(preferences.getLocalizedString(
											ImportedFurnitureWizardStepsPanel.class, "modelChangeButton.mnemonic"))
									.getKeyCode());
		}
	}
	
	/**
	 * Sets the texts of label and button of model panel with choice texts. 
	 */
	private void setModelChoiceTexts(UserPreferences preferences)
	{
		this.modelChoiceOrChangeLabel.setText(
				preferences.getLocalizedString(ImportedFurnitureWizardStepsPanel.class, "modelChoiceLabel.text"));
		this.modelChoiceOrChangeButton.setText(SwingTools.getLocalizedLabelText(preferences,
				ImportedFurnitureWizardStepsPanel.class, "modelChoiceButton.text"));
		if (!OperatingSystem.isMacOSX())
		{
			this.modelChoiceOrChangeButton
					.setMnemonic(
							KeyStroke
									.getKeyStroke(preferences.getLocalizedString(
											ImportedFurnitureWizardStepsPanel.class, "modelChoiceButton.mnemonic"))
									.getKeyCode());
		}
	}
	
	/**
	 * Returns the model name chosen for a file chooser dialog.
	 */
	private String showModelChoiceDialog(UserPreferences preferences, ContentManager contentManager)
	{
		return contentManager.showOpenDialog(this,
				preferences.getLocalizedString(ImportedFurnitureWizardStepsPanel.class, "modelChoiceDialog.title"),
				ContentManager.ContentType.MODEL);
	}
	
	/**
	 * Returns the icon content of the chosen piece.
	 * Icon is created once on demand of view's controller, because it demands either  
	 * icon panel being displayed, or an offscreen 3D buffer that costs too much to create at each yaw change.
	 */
	public Content getIcon()
	{
		try
		{
			return this.iconPreviewComponent.getIcon(400);
		}
		catch (IOException ex)
		{
			try
			{
				return new URLContent(new URL("file:/dummySweetHome3DContent"));
			}
			catch (MalformedURLException ex1)
			{
				return null;
			}
		}
	}
	
	/**
	 * Preview component for model changes. 
	 */
	private static abstract class AbstractModelPreviewComponent extends ModelPreviewComponent
	{
		public AbstractModelPreviewComponent(boolean pitchAndScaleChangeSupported)
		{
			super(pitchAndScaleChangeSupported);
		}
		
		/**
		 * Adds listeners to <code>controller</code> to update the rotation and the size of the piece model
		 * displayed by this component.
		 */
		protected void addSizeListeners(final ImportedFurnitureWizardController controller)
		{
			controller.addPropertyChangeListener(ImportedFurnitureWizardController.Property.BACK_FACE_SHOWN,
					new PropertyChangeListener()
					{
						public void propertyChange(PropertyChangeEvent ev)
						{
							setBackFaceShown(controller.isBackFaceShown());
						}
					});
			PropertyChangeListener sizeChangeListener = new PropertyChangeListener()
			{
				public void propertyChange(PropertyChangeEvent ev)
				{
					setModelRotationAndSize(controller.getModelRotation(), controller.getWidth(), controller.getDepth(),
							controller.getHeight());
				}
			};
			controller.addPropertyChangeListener(ImportedFurnitureWizardController.Property.MODEL_ROTATION,
					sizeChangeListener);
			controller.addPropertyChangeListener(ImportedFurnitureWizardController.Property.WIDTH, sizeChangeListener);
			controller.addPropertyChangeListener(ImportedFurnitureWizardController.Property.DEPTH, sizeChangeListener);
			controller.addPropertyChangeListener(ImportedFurnitureWizardController.Property.HEIGHT, sizeChangeListener);
		}
		
		/**
		 * Adds listener to <code>controller</code> to update the color of the piece 
		 * displayed by this component.
		 */
		protected void addColorListener(final ImportedFurnitureWizardController controller)
		{
			PropertyChangeListener colorChangeListener = new PropertyChangeListener()
			{
				public void propertyChange(PropertyChangeEvent ev)
				{
					setModelColor(controller.getColor());
				}
			};
			controller.addPropertyChangeListener(ImportedFurnitureWizardController.Property.COLOR, colorChangeListener);
		}
		
		/**
		 * Adds listener to <code>controller</code> to update the yaw of the piece icon
		 * displayed by this component.
		 */
		protected void addIconYawListener(final ImportedFurnitureWizardController controller)
		{
			PropertyChangeListener iconYawChangeListener = new PropertyChangeListener()
			{
				public void propertyChange(PropertyChangeEvent ev)
				{
					setViewYaw(controller.getIconYaw());
				}
			};
			controller.addPropertyChangeListener(ImportedFurnitureWizardController.Property.ICON_YAW,
					iconYawChangeListener);
		}
	}
	
	/**
	 * Preview component for model orientation. 
	 */
	private static class RotationPreviewComponent extends JPanel
	{
		private static final int COMPONENT_PREFERRED_WIDTH = Math.round(200 * SwingTools.getResolutionScale());
		
		private ModelPreviewComponent perspectiveViewComponent3D;
		private JLabel frontViewLabel;
		private ModelPreviewComponent frontViewComponent3D;
		private JLabel sideViewLabel;
		private ModelPreviewComponent sideViewComponent3D;
		private JLabel topViewLabel;
		private ModelPreviewComponent topViewComponent3D;
		private JLabel perspectiveViewLabel;
		private BranchGroup modelNode;
		
		public RotationPreviewComponent(UserPreferences preferences, final ImportedFurnitureWizardController controller)
		{
			createComponents(preferences, controller);
			layoutComponents();
		}
		
		public void setModel(Content model)
		{
			this.perspectiveViewComponent3D.setModel(model);
			this.frontViewComponent3D.setModel(model);
			this.sideViewComponent3D.setModel(model);
			this.topViewComponent3D.setModel(model);
		}
		
		/**
		 * Creates components displayed by this panel.
		 */
		private void createComponents(UserPreferences preferences, ImportedFurnitureWizardController controller)
		{
			Color backgroundColor = new Color(0xE5E5E5);
			this.perspectiveViewComponent3D = new ModelPreviewComponent(true);
			this.perspectiveViewComponent3D.setBackground(backgroundColor);
			addRotationListener(this.perspectiveViewComponent3D, controller, true);
			
			this.frontViewComponent3D = new ModelPreviewComponent(false, false, false);
			this.frontViewComponent3D.setViewYaw(0);
			this.frontViewComponent3D.setViewPitch(0);
			this.frontViewComponent3D.setParallelProjection(true);
			this.frontViewComponent3D.setBackground(backgroundColor);
			addRotationListener(this.frontViewComponent3D, controller, false);
			
			this.sideViewComponent3D = new ModelPreviewComponent(false, false, false);
			this.sideViewComponent3D
					.setViewYaw(Locale.getDefault().equals(Locale.US) ? -(float) Math.PI / 2 : (float) Math.PI / 2);
			this.sideViewComponent3D.setViewPitch(0);
			this.sideViewComponent3D.setParallelProjection(true);
			this.sideViewComponent3D.setBackground(backgroundColor);
			addRotationListener(this.sideViewComponent3D, controller, false);
			
			this.topViewComponent3D = new ModelPreviewComponent(false, false, false);
			this.topViewComponent3D.setViewYaw(0);
			this.topViewComponent3D.setViewPitch(-(float) Math.PI / 2);
			this.topViewComponent3D.setParallelProjection(true);
			this.topViewComponent3D.setBackground(backgroundColor);
			addRotationListener(this.topViewComponent3D, controller, false);
			
			this.frontViewLabel = new JLabel(
					preferences.getLocalizedString(ImportedFurnitureWizardStepsPanel.class, "frontViewLabel.text"));
			this.sideViewLabel = new JLabel(
					preferences.getLocalizedString(ImportedFurnitureWizardStepsPanel.class, "sideViewLabel.text"));
			this.topViewLabel = new JLabel(
					preferences.getLocalizedString(ImportedFurnitureWizardStepsPanel.class, "topViewLabel.text"));
			this.perspectiveViewLabel = new JLabel(preferences
					.getLocalizedString(ImportedFurnitureWizardStepsPanel.class, "perspectiveViewLabel.text"));
		}
		
		@Override
		public Dimension getPreferredSize()
		{
			return new Dimension(COMPONENT_PREFERRED_WIDTH,
					COMPONENT_PREFERRED_WIDTH + 4 + this.frontViewLabel.getPreferredSize().height * 2);
		}
		
		/**
		 * Adds listeners to <code>controller</code> to update the rotation of the piece model
		 * displayed by the 3D components.
		 */
		protected void addRotationListener(final ModelPreviewComponent viewComponent3D,
				final ImportedFurnitureWizardController controller, final boolean mainComponent)
		{
			controller.addPropertyChangeListener(ImportedFurnitureWizardController.Property.BACK_FACE_SHOWN,
					new PropertyChangeListener()
					{
						public void propertyChange(PropertyChangeEvent ev)
						{
							viewComponent3D.setBackFaceShown(controller.isBackFaceShown());
						}
					});
			if (mainComponent)
			{
				controller.addPropertyChangeListener(ImportedFurnitureWizardController.Property.MODEL,
						new PropertyChangeListener()
						{
							public void propertyChange(PropertyChangeEvent ev)
							{
								modelNode = null;
							}
						});
			}
			PropertyChangeListener rotationChangeListener = new PropertyChangeListener()
			{
				public void propertyChange(final PropertyChangeEvent ev)
				{
					viewComponent3D.setModelRotation(controller.getModelRotation());
					
					if (mainComponent && ev.getOldValue() != null && viewComponent3D.getModel() != null)
					{
						// Update size when a new rotation is provided
						if (modelNode == null)
						{
							ModelManager.getInstance().loadModel(viewComponent3D.getModel(),
									new ModelManager.ModelObserver()
									{
										public void modelUpdated(BranchGroup modelRoot)
										{
											modelNode = modelRoot;
											updateSize(controller, (float[][]) ev.getOldValue(),
													(float[][]) ev.getNewValue());
										}
										
										public void modelError(Exception ex)
										{}
									});
						}
						else
						{
							updateSize(controller, (float[][]) ev.getOldValue(), (float[][]) ev.getNewValue());
						}
					}
				}
				
				private void updateSize(final ImportedFurnitureWizardController controller, float[][] oldModelRotation,
						float[][] newModelRotation)
				{
					try
					{
						Transform3D normalization = ModelManager.getInstance().getNormalizedTransform(modelNode,
								oldModelRotation, 1f);
						Transform3D scaleTransform = new Transform3D();
						scaleTransform.setScale(
								new Vector3d(controller.getWidth(), controller.getHeight(), controller.getDepth()));
						scaleTransform.mul(normalization);
						
						// Compute rotation before old model rotation
						Matrix3f oldModelRotationMatrix = new Matrix3f(oldModelRotation[0][0], oldModelRotation[0][1],
								oldModelRotation[0][2], oldModelRotation[1][0], oldModelRotation[1][1],
								oldModelRotation[1][2], oldModelRotation[2][0], oldModelRotation[2][1],
								oldModelRotation[2][2]);
						oldModelRotationMatrix.invert();
						Transform3D backRotationTransform = new Transform3D();
						backRotationTransform.setRotation(oldModelRotationMatrix);
						backRotationTransform.mul(scaleTransform);
						
						// Compute size after new model rotation
						Matrix3f newModelRotationMatrix = new Matrix3f(newModelRotation[0][0], newModelRotation[0][1],
								newModelRotation[0][2], newModelRotation[1][0], newModelRotation[1][1],
								newModelRotation[1][2], newModelRotation[2][0], newModelRotation[2][1],
								newModelRotation[2][2]);
						Transform3D transform = new Transform3D();
						transform.setRotation(newModelRotationMatrix);
						transform.mul(backRotationTransform);
						
						Vector3f newSize = ModelManager.getInstance().getSize(modelNode, transform);
						controller.setWidth(newSize.x);
						controller.setHeight(newSize.y);
						controller.setDepth(newSize.z);
					}
					catch (IllegalArgumentException ex)
					{
						// Model is empty
					}
				}
			};
			controller.addPropertyChangeListener(ImportedFurnitureWizardController.Property.MODEL_ROTATION,
					rotationChangeListener);
		}
		
		/**
		 * Layouts components. 
		 */
		private void layoutComponents()
		{
			setLayout(new GridBagLayout());
			
			// Place the 4 3D components differently depending on US or other country
			if (Locale.getDefault().equals(Locale.US))
			{
				// Default projection view at top left
				add(this.perspectiveViewLabel, new GridBagConstraints(0, 0, 1, 1, 0, 0, GridBagConstraints.CENTER,
						GridBagConstraints.NONE, new Insets(0, 0, 2, 5), 0, 0));
				add(this.perspectiveViewComponent3D, new GridBagConstraints(0, 1, 1, 1, 1, 1, GridBagConstraints.CENTER,
						GridBagConstraints.BOTH, new Insets(0, 0, 5, 5), 0, 0));
				// Top view at top right
				add(this.topViewLabel, new GridBagConstraints(1, 0, 1, 1, 0, 0, GridBagConstraints.CENTER,
						GridBagConstraints.NONE, new Insets(0, 0, 2, 0), 0, 0));
				add(this.topViewComponent3D, new GridBagConstraints(1, 1, 1, 1, 1, 1, GridBagConstraints.CENTER,
						GridBagConstraints.BOTH, new Insets(0, 0, 5, 0), 0, 0));
				// Left view at bottom left
				add(this.sideViewLabel, new GridBagConstraints(0, 2, 1, 1, 0, 0, GridBagConstraints.CENTER,
						GridBagConstraints.NONE, new Insets(0, 0, 2, 5), 0, 0));
				add(this.sideViewComponent3D, new GridBagConstraints(0, 3, 1, 1, 1, 1, GridBagConstraints.CENTER,
						GridBagConstraints.BOTH, new Insets(0, 0, 0, 5), 0, 0));
				// Front view at bottom right
				add(this.frontViewLabel, new GridBagConstraints(1, 2, 1, 1, 0, 0, GridBagConstraints.CENTER,
						GridBagConstraints.NONE, new Insets(0, 0, 2, 0), 0, 0));
				add(this.frontViewComponent3D, new GridBagConstraints(1, 3, 1, 1, 1, 1, GridBagConstraints.CENTER,
						GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
			}
			else
			{
				// Right view at top left
				add(this.sideViewLabel, new GridBagConstraints(0, 0, 1, 1, 0, 0, GridBagConstraints.CENTER,
						GridBagConstraints.NONE, new Insets(0, 0, 2, 5), 0, 0));
				add(this.sideViewComponent3D, new GridBagConstraints(0, 1, 1, 1, 1, 1, GridBagConstraints.CENTER,
						GridBagConstraints.BOTH, new Insets(0, 0, 5, 5), 0, 0));
				// Front view at top right
				add(this.frontViewLabel, new GridBagConstraints(1, 0, 1, 1, 0, 0, GridBagConstraints.CENTER,
						GridBagConstraints.NONE, new Insets(0, 0, 2, 0), 0, 0));
				add(this.frontViewComponent3D, new GridBagConstraints(1, 1, 1, 1, 1, 1, GridBagConstraints.CENTER,
						GridBagConstraints.BOTH, new Insets(0, 0, 5, 0), 0, 0));
				// Default projection view at bottom left
				add(this.perspectiveViewLabel, new GridBagConstraints(0, 2, 1, 1, 0, 0, GridBagConstraints.CENTER,
						GridBagConstraints.NONE, new Insets(0, 0, 2, 5), 0, 0));
				add(this.perspectiveViewComponent3D, new GridBagConstraints(0, 3, 1, 1, 1, 1, GridBagConstraints.CENTER,
						GridBagConstraints.BOTH, new Insets(0, 0, 0, 5), 0, 0));
				// Top view at bottom right
				add(this.topViewLabel, new GridBagConstraints(1, 2, 1, 1, 0, 0, GridBagConstraints.CENTER,
						GridBagConstraints.NONE, new Insets(0, 0, 2, 0), 0, 0));
				add(this.topViewComponent3D, new GridBagConstraints(1, 3, 1, 1, 1, 1, GridBagConstraints.CENTER,
						GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
			}
		}
	}
	
	/**
	 * Preview component for model attributes. 
	 */
	private static class AttributesPreviewComponent extends AbstractModelPreviewComponent
	{
		public AttributesPreviewComponent(ImportedFurnitureWizardController controller)
		{
			super(true);
			addSizeListeners(controller);
			addColorListener(controller);
		}
	}
	
	/**
	 * Preview component for model icon. 
	 */
	private static class IconPreviewComponent extends AbstractModelPreviewComponent
	{
		private static final int ICON_SIZE = Math.round(128 * SwingTools.getResolutionScale());
		
		private ImportedFurnitureWizardController controller;
		
		public IconPreviewComponent(ImportedFurnitureWizardController controller)
		{
			super(false);
			this.controller = controller;
			addSizeListeners(controller);
			addColorListener(controller);
			addIconYawListener(controller);
			
			Color backgroundColor = UIManager.getColor("window");
			if (backgroundColor == null)
			{
				backgroundColor = Color.LIGHT_GRAY;
			}
			setBackground(backgroundColor);
		}
		
		@Override
		public Dimension getPreferredSize()
		{
			Insets insets = getInsets();
			return new Dimension(ICON_SIZE + insets.left + insets.right, ICON_SIZE + insets.top + insets.bottom);
		}
		
		/**
		 * Sets the <code>yaw</code> angle used by view platform transform.
		 */
		protected void setViewYaw(float viewYaw)
		{
			super.setViewYaw(viewYaw);
			this.controller.setIconYaw(viewYaw);
		}
	}
}
